/*
 * Test env_restore
 *
 * Copyright 2022 and 2023 Odin Kroeger.
 *
 * This file is part of suCGI.
 *
 * suCGI is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * suCGI is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
 */

#define _XOPEN_SOURCE 700

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <float.h>
#include <math.h>
#include <regex.h>
#include <setjmp.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "../env.h"
#include "../macros.h"
#include "../params.h"
#include "../str.h"
#include "types.h"
#include "libutil/abort.h"
#include "libutil/array.h"
#include "libutil/str.h"
#include "libutil/types.h"


/*
 * Constants
 */

/* Maximum number of environment variables for testing */
#define MAX_TEST_NVARS (MAX_NVARS + 1U)

/* Maximum string length for testing */
#define MAX_TEST_STR_LEN (MAX_STR_LEN + 1U)

/* Format for creating environment variables */
#define VAR_FORMAT "var%0*td= "


/*
 * Data types
 */

/* Mapping of constant inputs to constant outputs */
typedef struct {
    const char *const vars;         /* Variables to set */
    const char *const *env;         /* Resulting environmnet */
    Error retval;                   /* Return value */
    int signal;                     /* Signal caught, if any */
} EnvSetManyArgs;


/*
 * Main
 */

int
main(void)
{
    assert(MAX_NVARS > 0);
    assert((uintmax_t) MAX_NVARS < (uintmax_t) INT_MAX);
    /* NOLINTNEXTLINE(readability-magic-numbers); false positive */
    assert(MAX_NVARS < DBL_MAX);

    /* RATS: ignore; format is short and a literal */
    const int maxndigits = snprintf(NULL, 0, "%u", MAX_NVARS);

    /* RATS: ignore; no string expansion */
    const int varlen = snprintf(NULL, 0, VAR_FORMAT,
                                maxndigits, (ptrdiff_t) MAX_NVARS);
    if (varlen < 1) {
        err(ERROR, "snprintf");
    }

    assert((uintmax_t) varlen < (uintmax_t) SIZE_MAX);
    const int strsize = (MAX_NVARS + 1) * varlen + 1;
    /* cppcheck-suppress misra-c2012-11.5; bad advice for malloc */
    char *const hugenvars = malloc((size_t) strsize);
    if (hugenvars == NULL) {
        err(ERROR, "malloc");
    }

    const size_t varsize = (size_t) varlen + 1U;
    for (ptrdiff_t i = 0; i <= MAX_NVARS; ++i) {
        assert(i < PTRDIFF_MAX / varlen);
        ptrdiff_t startidx = i * varlen;

        errno = 0;
        /* RATS: ignore; format is a literal and expansion is bounded */
        const int nbytes = snprintf(&hugenvars[startidx], (size_t) varsize,
                                    VAR_FORMAT, maxndigits, i);
        if (nbytes < 1) {
            err(ERROR, "snprintf");
        }
    }

    /* RATS: ignore; used safely */
    char longvar[MAX_VAR_LEN] = {0};
    (void) memset(longvar, 'x', sizeof(longvar) - 1U);
    longvar[1] = '=';

    /* RATS: ignore; used safely */
    char hugevar[MAX_VAR_LEN + 1] = {0};
    (void) memset(hugevar, 'x', sizeof(hugevar) - 1U);
    hugevar[1] = '=';

    /* RATS: ignore; used safely */
    char longname[MAX_VAR_LEN] = {0};
    (void) memset(longname, 'x', sizeof(longname) - 1U);
    longname[MAX_VARNAME_LEN - 1U] = '=';

    /* RATS: ignore; used safely */
    char hugename[MAX_VAR_LEN];
    (void) memset(hugename, 'x', sizeof(hugename) - 1U);
    hugename[MAX_VARNAME_LEN] = '=';

    const EnvSetManyArgs cases[] = {
        /* Bad arguments */
#if !defined(NDEBUG) && defined(NATTR)
        {NULL, (const char * []) {NULL}, OK, SIGABRT},
#endif

        /* Null tests */
        {"", (const char * []) {NULL}, OK, 0},

        /* Too many variables */
        {hugenvars, (const char * []) {NULL}, ERR_NELEMS, 0},

        /* Long but legal variable */
        {longvar, (const char * []) {longvar, NULL}, OK, 0},

        /* Long but legal variable name */
        {longname, (const char * []) {longname, NULL}, OK, 0},

        /* Variable that is too long */
        {hugevar, (const char * []) {NULL}, ERR_LEN, 0},

        /* Variable with a name that is too long */
        {hugename, (const char * []) {NULL}, ERR_LEN, 0},

        /* Simple tests */
        {"foo=foo", (const char * []) {"foo=foo", NULL}, OK, 0},
        {
            "foo=foo bar=bar",
            (const char * []) {
                "foo=foo",
                "bar=bar",
                NULL
            },
            OK,
            0
        },

        {"foo", (const char * []) {NULL}, ERR_BAD, 0},
        {"=foo", (const char * []) {NULL}, ERR_BAD, 0},

        /* Illegal names */
        {" foo=foo",   (const char * []) {NULL}, ERR_BAD, 0},
        {"0foo=foo",   (const char * []) {NULL}, ERR_BAD, 0},
        {"*=foo",      (const char * []) {NULL}, ERR_BAD, 0},
        {"foo =foo",   (const char * []) {NULL}, ERR_BAD, 0},
        {"$(foo)=foo", (const char * []) {NULL}, ERR_BAD, 0},
        {"`foo`=foo",  (const char * []) {NULL}, ERR_BAD, 0},

        /* Odd but legal values */
        {
            .vars = "SSL_CLIENT_S_DN_C_0= SSL_CLIENT_S_DN_C_1==",
            .env = (const char * []) {
                "SSL_CLIENT_S_DN_C_0=",
                "SSL_CLIENT_S_DN_C_1==",
                NULL
            },
            .retval = OK,
            .signal = 0
        },

        /* Real-world tests */
        {
            .vars = "LANG=en_GB.UTF-8 IFS= SHELL=/bin/sh",
            .env = (const char * []) {
                "LANG=en_GB.UTF-8",
                "IFS=",
                "SHELL=/bin/sh",
                NULL
            },
            .retval = OK,
            .signal = 0
        }
    };

    volatile int result = PASS;
    char *null = NULL;

    /* Run tests */
    for (volatile size_t i = 0; i < NELEMS(cases); ++i) {
        const EnvSetManyArgs args = cases[i];

        if (sigsetjmp(abort_env, 1) == 0) {
            environ = &null;

            if (args.signal != 0) {
                warnx("the next test should fail an assertion.");
            }

            (void) abort_catch(err);
            const Error retval = env_set_many(args.vars);
            (void) abort_reset(err);

            if (retval != args.retval) {
                result = FAIL;
                warnx("(%s) -> %u [!]", args.vars, retval);
            }

            if (retval == OK) {
                volatile size_t nexpected = 0;
                while (args.env[nexpected] != NULL) {
                    assert(nexpected < SIZE_MAX);
                    ++nexpected;
                }

                volatile size_t nactual = 0;
                while (environ[nactual] != NULL) {
                    assert(nexpected < SIZE_MAX);
                    ++nactual;
                }

                if (!array_eq(
                    (const void *) args.env, nexpected, sizeof(*args.env),
                    (const void *) environ, nactual, sizeof(*environ),
                    (CompFn) str_cmp_ptrs
                )) {
                    result = FAIL;
                    warnx("(%s) --> <environ> [!]", args.vars);
                }
            }
        }

        if (abort_signal != args.signal) {
            result = FAIL;
            warnx("(%s) ^ %s [!]", args.vars, strsignal((int) abort_signal));
        }
    }

    /* Superfluous, but some versions of ASan think otherwise */
    free(hugenvars);

    return result;
}
